Restore blocking mode after successful ConnectAsync on Unix#124200
Restore blocking mode after successful ConnectAsync on Unix#124200liveans wants to merge 2 commits intodotnet:mainfrom
Conversation
|
Tagging subscribers to this area: @karelz, @dotnet/ncl |
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/tests/FunctionalTests/Connect.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SafeSocketHandle.Unix.cs
Show resolved
Hide resolved
🤖 Copilot Code Review — PR #124200Holistic AssessmentMotivation: The optimization goal is valid. After Approach: The implementation adds Summary: Detailed Findings
|
ba10f3d to
49a3f53
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs:688
- The InvokeCallback override only invokes the callback and calls RestoreBlocking when buffer.Length is 0. However, when a connect operation fails (ErrorCode != Success) and a buffer was provided (via DoOperationConnectEx), the callback will never be invoked and RestoreBlocking won't be called.
This can occur when using ConnectEx with a buffer on Unix systems. The callback must always be invoked when the operation is complete, regardless of whether there's a buffer or whether the operation succeeded or failed. RestoreBlocking should also be called on error paths to restore the socket to blocking mode after a failed connection attempt.
Consider updating the logic to:
- Always call RestoreBlocking when the connect fails (ErrorCode != Success), regardless of buffer.Length
- Always invoke the callback when the operation is complete and no follow-up Send is needed
public override void InvokeCallback(bool allowPooling)
{
var cb = Callback!;
int bt = BytesTransferred;
Memory<byte> sa = SocketAddress;
SocketError ec = ErrorCode;
Memory<byte> buffer = Buffer;
if (buffer.Length == 0)
{
AssociatedContext._socket.RestoreBlocking();
// Invoke callback only when we are completely done.
// In case data were provided for Connect we may or may not send them all.
// If we did not we will need follow-up with Send operation
cb(bt, sa, SocketFlags.None, ec);
}
}
| [Fact] | ||
| public async Task ConnectAsync_Failure_SocketIsRestoredToBlocking() | ||
| { | ||
| using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); | ||
|
|
||
| await Assert.ThrowsAsync<SocketException>(async () => | ||
| await client.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 1))); | ||
|
|
||
| Assert.False(IsSocketNonBlocking(client)); | ||
| } |
There was a problem hiding this comment.
The test suite is missing coverage for a failed ConnectAsync with a data buffer. When using ConnectEx with a buffer (via SocketAsyncEventArgs.SetBuffer), if the connection fails, the callback may not be invoked due to the bug in ConnectOperation.InvokeCallback (lines 671-688 in SocketAsyncContext.Unix.cs).
Consider adding a test that:
- Creates a SocketAsyncEventArgs with a buffer via SetBuffer
- Attempts to connect to an unreachable endpoint (e.g., port 1)
- Verifies that the Completed callback is invoked with an error
- Verifies that the socket is restored to blocking mode after the failed connect
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncContext.Unix.cs
Outdated
Show resolved
Hide resolved
49a3f53 to
b8c9054
Compare
|
/azp run runtime-libraries-coreclr outerloop |
|
Azure Pipelines successfully started running 1 pipeline(s). |
b8c9054 to
387bea9
Compare
|
/azp run runtime-libraries-coreclr outerloop |
|
Azure Pipelines successfully started running 1 pipeline(s). |
| var saea = new SocketAsyncEventArgs(); | ||
| saea.RemoteEndPoint = (IPEndPoint)listener.LocalEndPoint!; | ||
| saea.SetBuffer(new byte[] { 1, 2, 3 }, 0, 3); | ||
|
|
||
| var tcs = new TaskCompletionSource(); | ||
| saea.Completed += (_, _) => tcs.SetResult(); | ||
|
|
||
| if (!client.ConnectAsync(saea)) | ||
| { | ||
| tcs.SetResult(); | ||
| } | ||
|
|
||
| await tcs.Task; | ||
|
|
||
| Assert.Equal(SocketError.Success, saea.SocketError); | ||
|
|
||
| // When buffer > 0, the socket stays non-blocking because SendToAsync | ||
| // may have been used to send the initial data after connect. | ||
| Assert.True(IsSocketNonBlocking(client)); | ||
|
|
||
| saea.Dispose(); | ||
| } |
There was a problem hiding this comment.
ConnectAsync_WithBuffer manually calls saea.Dispose() at the end of the test. If the test fails early (e.g., an Assert throws) the SocketAsyncEventArgs won't be disposed, which can leak resources and affect subsequent tests. Prefer wrapping it in a using declaration/statement or disposing it in a finally block.
Summary
On Unix, after a successful
ConnectAsynccompletion, this PR restores the underlying socket to blocking mode if the user hasn't explicitly setSocket.Blocking = false. Thisoptimizes subsequent synchronous operations (
Send/Receive) by using native blocking syscalls instead of emulating blocking on top of epoll/kqueue.When
SendAsync/ReceiveAsync(or other async I/O operations) are called later, the socket is switched back to non-blocking mode via the existingSetHandleNonBlocking()mechanism.
Motivation
Currently, once any async operation is performed on a Unix socket, the underlying file descriptor is permanently set to non-blocking mode via
fcntl(O_NONBLOCK). Subsequentsynchronous operations must then emulate blocking behavior in managed code using epoll/kqueue, which has performance overhead compared to native blocking syscalls.
This change benefits the common usage pattern: